Hito 3: Estudio de las recomendaciones de Steam¶

Grupo 03:

  • Adolfo Arenas P.
  • Alejandro Mori A.
  • Ignacio Humire S.
  • Leonardo Rikhardsson
  • Mario Benavente C.

1. Introducción

1.1 ¿Qué es Steam?

  Steam es una plataforma de distribución digital de videojuegos, software y contenido multimedia creada por Valve Corporation, enfocada en entregar dichos contenidos para computadores con sistemas operativos Windows, macOS y Linux.

Esta aplicación garantiza un ecosistema donde jugadores de todos lados del mundo puedan interactuar entre ellos mediante foros públicos, conversaciones por grupos/comunidades e incluso chats privados con tus amigos, sin mencionar opciones como juego multijugador en línea, compartir juegos entre amigos y un entorno virtual de multijugador local (en resumen, poder jugar un multijugador local mediante una conexión a un amigo).

Sumado a esto, constantemente buscan estimular la competitividad entre sus usuarios mediante opciones como estadísticas de juego, marcador de horas jugadas, integración de logros (con la opción de añadir logros "ocultos": no se menciona cómo obtenerlos hasta que el jugador cumple los requerimientos para conseguirlo), entre otros.

Finalmente, poseen herramientas como la recomendación de juegos por usuarios, la cual permite que los jugadores puedan dar a conocer sus opiniones, dando espacio para la opinión pública, de manera que pueden recomendar o no recomendar un juego, con la opción de incluir un párrafo donde argumentar su decisión.

1.2 Motivación

  La industria de los videojuegos está en constante crecimiento, con un aumento significativo en el número de jugadores a nivel mundial, generando una mayor demanda y expectativas por parte de los consumidores hacia las empresas desarrolladoras de videojuegos. Esto a su vez ha provocado un aumento en la cantidad de juegos lanzados en el mercado, desde aquellas producciones de las distribuidoras más importanes del mercado (conocidas como AAA) hasta los creados por desarrolladores independientes (indies). Lo anterior puede llevar a los desarrolladores de juegos preguntarse: ¿Cómo puedo hacer que mi proyecto se venda en un mercado tan saturado?

Como grupo, creemos que es fundamental analizar previamente el sector del mercado al que un desarrollador desea dirigirse considerando el tipo de producto que desea lanzar, si posee o no una empresa que lo respalde (publisher), entre otras cosas. Por lo anterior, mediante un proyecto de minería de datos sobre DataSets de Steam, buscamos proporcionar información valiosa que permita a los desarrolladores tomar decisiones informadas y aumentar sus probabilidades de éxito de ventas al lanzar su juego en la misma plataforma.

2. Exploración de Datos

  Para la exploración de datos se usarán dos DataSets:

  • Steam Games Dataset: Contiene información sobre cada juego (género de juego, precio, cantidad de reviews, etc).

  • Steam Reviews Dataset 2021: Contiene información más detallada sobre las reviews de usuarios por juego, incluyendo la reseña que escribió el usuario acerca de un producto.

A pesar de que las tablas tienen distintos atributos, podemos realizar Joins para unirlas, ya que comparten el atributo app_id (ID el cual es dado por Steam y es único para cada juego). Cada destacar que dado el tamaño de este dataset, solo usaremos un cuarto de los datos, que son alrededor de 12 millones de filas.

In [ ]:
# @title 2.1 Imports
from nltk.corpus import stopwords
from wordcloud import WordCloud

import matplotlib.pyplot as plt
import plotly.express as px
import pandas as pd
import numpy as np
import os
import csv
import json
import nltk
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
from sklearn.utils.multiclass import unique_labels
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, mean_absolute_error ,r2_score
In [ ]:
# Codigo para que la parte interactiva se vea en el HTML

import plotly.io as pio
pio.renderers.default='notebook'
In [ ]:
# @title 2.2 Exportación de Drive
from google.colab import drive
drive.mount('/content/drive')
Mounted at /content/drive

Primero cargamos los datos que serán usados para nuestra exploración de datos.

In [ ]:
# @title 2.3 Carga de DataFrames
df_games = pd.read_csv('/content/drive/MyDrive/CC5205/games.csv', encoding="UTF-8")
df_steam_reviews = pd.read_csv('/content/drive/MyDrive/CC5205/steam_reviews.csv', encoding="UTF-8", nrows=12043389)
In [ ]:
# @title 2.4  WordCloud

# Asegurarse de que la columna 'release_date' esté en formato de fecha
df_games['release_date'] = pd.to_datetime(df_games['release_date'], errors='coerce')

# Extraer el año de la fecha de lanzamiento
df_games['year'] = df_games['release_date'].dt.year

# Filtrar los datos para eliminar aquellos sin año de lanzamiento válido
df_games = df_games.dropna(subset=['year'])

# Obtener los años únicos y ordenarlos
years = sorted(df_games['year'].unique())

# Crear una nube de palabras para cada año
for year in years:
    # Filtrar los juegos por año
    juegos_ano = df_games[df_games['year'] == year]

    # Crear un diccionario para contar las frecuencias de cada género
    genero_frecuencias = {}

    # Contar las frecuencias de cada género
    for generos in juegos_ano['genres']:
        if pd.notna(generos):  # Verificar que no sea NaN
            for genero in generos.split(','):
                genero = genero.strip()  # Quitar espacios en blanco alrededor
                if genero in genero_frecuencias:
                    genero_frecuencias[genero] += 1
                else:
                    genero_frecuencias[genero] = 1

    # Generar la nube de palabras usando las frecuencias
    wordcloud = WordCloud(width=800, height=400, background_color='white').generate_from_frequencies(genero_frecuencias)

    # Mostrar la nube de palabras
    plt.figure(figsize=(10, 5))
    plt.imshow(wordcloud, interpolation='bilinear')
    plt.title(f'Nube de Palabras de Géneros para el Año {int(year)}')
    plt.axis('off')  # Desactivar los ejes
    plt.show()

  En la figura tenemos plasmados distintos WordClouds para los géneros de videojuegos que hayan sido lanzados entre 1997 y 2025, ordenados por año de salida.

Como primera iteración, se decidió hacer un WordCloud para los juegos en general, sin ningún filtro asociado. El resultado fue un gráfico muy denso, lo que hacía difícil intentar deducir algo. Además, la forma en la que se realizó el WordCloud consideraba juegos con más de un género como géneros únicos (ej: si un juego tenía los géneros “Indie” y “Action”, el WordCloud lo mostraba como “Indie Action”, lo cual era erróneo).

Por ello se decidió separarlo por años, pues permite captar tendencias de géneros populares a lo largo del tiempo, además de identificar cuándo se popularizó un cierto género en caso de querer hacer una investigación más dirigida. Finalmente, arreglamos los géneros de cada juego para separarlos por comas, separándolos como “Indie Action” en sus componentes, “Indie” y “Action”.

De estos datos podemos ver una clara tendencia de géneros, como lo fue el surgimiento de los juegos independientes (indie) lo que se puede relacionar a la facilidad de acceso a un computador durante los últimos años en comparación a 25 años atrás, además de un aumento en el material de aprendizaje para crear videojuegos. Otro caso de estas tendencias son los juegos Early access, siendo productos no terminados que se lanzan al mercado, donde los desarrolladores trabajan junto a los jugadores para recibir feedback y arreglar errores.

In [ ]:
# @title 2.5 Diagrama de Dispersión
# Crear el scatter plot
fig_scatter = px.scatter(df_games,
                         x='price',
                         y='recommendations',
                         color='metacritic_score',
                         title='Scatter Plot: Precio vs Número Total de Reseñas',
                         labels={'price': 'Precio (USD)', 'recommendations': 'Número Total de Recomendaciones'})

fig_scatter.show()

  Para este análisis se buscará una posible relación entre el precio de los juegos y la cantidad de reviews de los usuarios. La mejor herramienta para estudiar esto es un diagrama de dispersión. Además, se mostrarán los outliers para dar cuenta de situaciones particulares dentro de la plataforma.

Sumado a esto, dicho diagrama de dispersión representará la ubicación del juego en el gráfico con un punto coloreado según su puntuación en Metacritic.

Metacritic es una página que recolecta las reseñas de algunos de los mejores críticos de las industrias de la televisión, cine, videojuegos, entre otros, para luego entregar un puntaje guiado por estas. Es muy útil en este contexto revisar la puntuación que un juego puede tener pues las puntuaciones entregadas por esta página suelen influir en la opinión del público general a la hora de valorar un nuevo lanzamiento, a la vez que entrega cierta credibilidad a dicha puntuación, útil para cuando un consumidor busca comprar un juego nuevo y necesita una opinión respaldada.

Del gráfico podemos notar que todos los juegos sobre los 100 dólares en adelante tiene una puntuación alrededor de 0 ya sea por una mala puntuación de parte de metacritic o simplemente no fueron puntuados, lo anterior puede deberse al sobreprecio sobre los mismos. Por otro lado, notamos que juegos con puntuaciones altas en Metacritic suelen ser juegos de no más de 60 dólares (que suele ser el precio estándar para producciones de grandes empresas) y con al menos 10 mil reseñas. En relación a lo anterior se observa una tendencia en donde a menor precio posee un juego mayor es su número de reseñas en la plataforma, esto se debe a la facilidad que hay para acceder al producto.

In [ ]:
# @title 2.6 Diagrama de Dispersión con límites

# Crear el scatter plot
fig_scatter = px.scatter(df_games,
                         x='price',
                         y='recommendations',
                         color='metacritic_score',
                         title='Scatter Plot: Precio vs Número Total de Reseñas',
                         labels={'price': 'Precio (USD)', 'recommendations': 'Número Total de Recomendaciones'},
                         range_x=[0,100],
                         range_y=[1,1000000])

fig_scatter.show()

  Se muestra el mismo gráfico que en la parte anterior pero con la eliminación de outliers. En este gráfico se muestra una clara tendencia a establecer precios en múltiplos de cinco y estar por debajo de los 30 dólares. La diferencias de precios que se observan podrían depender de la producción detrás de estos, por ejemplo: un juego con un publisher reconocido detrás tendrá, por lo general, un precio de 60 dólares; un juego hecho por desarrolladores independientes rara vez superará los 40 dólares.

In [ ]:
# @title 2.7 Histograma de tiempo de juego por rango de precios

# Agrupa los datos por rango de precios
price_bins = [0, 10, 20, 30, 40, 50, 60, 70, 80]
df_games['price_range'] = pd.cut(df_games['price'], bins=price_bins)

# Calcula la media del tiempo de juego promedio para cada rango de precios
games_price_range = df_games.groupby('price_range')['average_playtime_forever'].mean().reset_index()

# Convierte el tiempo de juego de minutos a horas
games_price_range['average_playtime_forever'] = games_price_range['average_playtime_forever'] / 60

# Convierte los intervalos a cadenas
games_price_range['price_range'] = games_price_range['price_range'].astype(str)

# Crea el histograma
fig = px.bar(games_price_range, x='price_range', y='average_playtime_forever',
             title='Promedio de tiempo de juego por rango de precios',
             labels={'price_range': 'Rango de precios', 'average_playtime_forever': 'Promedio de tiempo de juego (horas)'},
             color_discrete_sequence=['orange'])

# Muestra el histograma
fig.show()

  Podemos notar que el promedio de horas de juego incrementa conforme sube el precio, alcanzando su punto máximo en los 60 dólares, que es el precio estándar máximo en la industria de los videojuegos. Esta información puede ser útil para que los desarrolladores estimen cuántas horas debería durar su juego ó cuanto podrían cobrar por él.

In [ ]:
# @title 2.7 WordCloud sobre palabras presentes en reseñas de juegos del género "Farming sim"

# Descargar stopwords en inglés
nltk.download('stopwords')
english_stopwords = set(stopwords.words('english'))

# Agregar las palabras específicas a las stopwords
custom_stopwords = {'game', 'really', 'play', 'like', 'thing', 'would', 'get', 'one', 'even', 'good', 'want', "I'm", 'much', 'make', 'still'}
all_stopwords = english_stopwords.union(custom_stopwords)

df_jrpg = df_games[df_games['tags'].str.contains('Farming Sim', na=False)]
id_jrpgs = df_jrpg['app_id']

df_steam_reviews_english = df_steam_reviews[(df_steam_reviews['language'] == 'english') & (df_steam_reviews['app_id'].isin(id_jrpgs))]

def create_word_cloud(reviews, title, max_words, stopwords):

    # Remover palabras vacías que no agregan nada al gráfico
    def remove_stopwords(text):
        words = text.split()
        filtered_words = [word for word in words if word.lower() not in stopwords]
        return ' '.join(filtered_words)

    cleaned_reviews = [remove_stopwords(review) for review in reviews]

    text = ' '.join(cleaned_reviews)

    wordcloud = WordCloud(width=800, height=400, max_words=max_words, background_color='white', stopwords=stopwords).generate(text)
    plt.figure(figsize=(8, 4))
    plt.imshow(wordcloud, interpolation='bilinear')
    plt.title(title + ' Word Cloud')
    plt.axis('off')
    plt.show()

positive_reviews = df_steam_reviews_english[df_steam_reviews_english['recommended'] == True]['review'].astype(str).sample(n=1000).tolist()
negative_reviews = df_steam_reviews_english[df_steam_reviews_english['recommended'] == False]['review'].astype(str).sample(n=1000).tolist()

create_word_cloud(positive_reviews, 'Reviews positivas para Farming simulator', 1000, all_stopwords)
create_word_cloud(negative_reviews, 'Reviews negativas para Farming simulator', 1000, all_stopwords)
[nltk_data] Downloading package stopwords to /root/nltk_data...
[nltk_data]   Unzipping corpora/stopwords.zip.

  Obteniedo los comentarios en inglés para un juego del género Farming Sim podemos armar un WordCloud para ver cuales son las palabras más usadas al momento de reseñar un juego.

Se puede observar que la reseñas positivas mencionan frecuentemente Harvest Moon y Stardew Valley, juegos los cuales se podrían utilizar por un desarrollador de videojuegos como referencias para crear juegos de este género. Por otra parte, para las reseñas negativas se aprecian palabras como "graphic" (calidad gráfica), "time", "bug", "boring", etc. Estás últimas son palabras a tomar en cuenta a la hora de trabajar ya que si se descuidan estos aspectos podrían llevar a que el producto tenga malas reseñas y con ello bajas ventas.

3. Preguntas y problemas

  A partir de nuestra motivación y lo encontrado en la exploración de datos, como grupo nos surgieron las siguientes preguntas que encontramos interesantes para estudiar el DataSet:

Pregunta 1 (Clasificadores)¶

¿Qué parámetros influyen más en la cantidad de ventas totales?¶

  Para responder a esta pregunta haremos uso de clasificadores que usen distintos pares de atributos cada uno con el objetivo de medir cual de ellos tiene mejor desempeño que el resto.

En primer lugar creamos la función run_classifier(clf, X, Y, num_test=100) la cuál se encargará de entrenar un modelo a partir de un clasificador clf para predecir la variable y en función de X. Cabe destacar que se usará el 30% del conjunto de datos para entrenar el modelo y el porcentaje restante para evaluar el desempeño del mismo. Finalmente, para medir el desempeño de cada clasificador se usará el promedio de las métricas precision, recall y f1-score.

Para la comparación de los distintos clasificadores usaremos:

  • Dummy Classifier :

    Es un clasificador muy simple ya que genera predicciones ignorando las características del input. Solo se usa como punto de comparación para clasificadores más complejos.

  • Decision Tree :

    Utiliza un arból de decisión como estructura, en donde cada nodo interno representa una decisión sobre una característica.

  • Gaussian Naive Bayes :

    Está basado en el Teorema de Bayes, el cual asume independecia entre las variables. En específico, el tipo Gaussian asume que los datós numéricos presentan una distribución normal.

  • K-Nearest Neighbors (KNN) :

    Clasifica un dato según las clases de sus K vecinos más cercanos. Suele utilizar la distancia euclidiana como métrica.

Luego de obtener los resultados, graficaremos usando una matriz de confusión los resultados del clasificador con mejor desempeño, esto con el objetivo de facilitar la visualización de los datos.

Pregunta 2 (Regresión)¶

¿Se puede predecir la cantidad de juegos con género "X" que habrán en un año determinado?¶

 
Si una persona quiere desarrollar un videojuego del género "Action" el 2025, sería útil saber cuántos videojuegos del mismo género saldrán ese año para tener una idea de contra cuántos videojuegos distintos tendrá que competir el producto. Pero, ¿Podemos predecir la cantidad de juegos del género que saldrán ese año?

  • Asumiendo que tiene comportamiento lineal, se comprobará si se logra obtener una buena predicción haciendo uso de regresión lineal

Preprocesamiento:

  • La columna "release_date" del DataSet de Steam contiene la fecha de salida del videojuego con el formato: "mes día, año", por lo que habría que crear una columna que contenga solamente el año de salida.

  • La columna "genres" contiene elementos de la forma: "género1, género2, género3", por lo que tendríamos que extraer solamente las filas que contengan la palabra "x" en su género.

  • Agrupar los videojuegos por año de lanzamiento para facilitar la regresión lineal.

  • Evaluar si se debe restringir el rango de años para entrenar la regresión

Experimento:

  • Se utiliza la regresión lineal, donde la variable independiente es el año y la dependiente la cantidad de juegos con género "x" en ese año.

Análisis:

  • Se graficará el resultado de la regresión tomando la pendiente y el intercepto. Junto con esto también graficaremos los puntos utilizados para realizar la regresión, con el objetivo de comparar visualmente la predicción con los datos reales.

  • Se calculará el Error Absoluto Medio y el coeficiente de determinación R2 para decidir si la técnica tuvo un buen desempeño al predecir los valores.

Pregunta 3 (Clustering)¶

¿Es posible identificar grupos de juegos (según precio o número de reviews positivas, por ejemplo) que ayuden a entender las características comunes de juegos exitosos o populares?¶

Otra forma de frasearlo es plantear la posibilidad de que un desarrollador pueda hallar grupos de juegos bajo ciertos valores similares con respecto a estos atributos escogidos.

Por ejemplo, ¿podemos hacer un grupo "Best Sellers" para juegos con muchas recomendaciones, donde la mayoría son positivas? ¿Podemos encontrar un arquetipo del estilo "Niche Games", basada en juegos buenos pero con pocas recomendaciones?

Preprocesamiento:

  • Para el dataset, usamos los atributos que influyen a las ventas: 'price' (precio del juego), 'positive' (cantidad de reseñas positivas), 'negative' (cantidad de reseñas negativas) y 'recommendations' (cantidad de veces que un jugador recomendó el juego). Después, filtramos los datos de forma que 'positive', 'negative' y 'recommendations' deben ser valores positivos.

Experimento:

  • Aplicamos PCA a los datos, para obtener una visualización de éstos en dimensión reducida (en este caso, 2D).

  • Buscamos con el método del codo la cantidad de Clusters para el problema, y aplicamos algún clasificador para encontrar dichos Clusters: en este caso se usará K-means, Aglomerative Clustering por método Ward, y DBSCAN (tres métodos con acercamientos distintos a la manera en que realizan clustering).

Análisis:

  • Por método del codo se buscará la cantidad de clusters en K-means.

  • Se visualizarán los Clusters obtenidos por Clustering Jerárquico de método 'Linkage: Ward', y se definirá la cantidad de Clusters mediante una altura de corte pertinente.

  • Se aplicará DBSCAN a los datos, en base a un valor eps obtenido analíticamente, y un valor minPts estándar.

  • Se va a calcular el Coeficiente de Silhouette para evaluar la calidad de los Clusters generados.

4. Experimentación y resultados

Pregunta 1¶

In [ ]:
# @title  
from sklearn.metrics import f1_score, recall_score, precision_score
from sklearn.model_selection import train_test_split
import numpy as np

def run_classifier(clf, X, y, num_tests=100):
    metrics = {'precision': [], 'recall': [], 'f1-score': []}

    for _ in range(num_tests):
        X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.30, random_state=15, stratify=y)

        clf.fit(X_train, y_train)
        predictions = clf.predict(X_test)

        metrics['precision'].append(precision_score(y_test, predictions,average='weighted', zero_division=0))
        metrics['recall'].append(recall_score(y_test, predictions, average='weighted', zero_division=0))
        metrics['f1-score'].append(f1_score(y_test, predictions, average='weighted', zero_division=0))
    return metrics
In [ ]:
# @title  Clasificadores elegidos
from sklearn.dummy import DummyClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.naive_bayes import GaussianNB  # naive bayes
from sklearn.neighbors import KNeighborsClassifier #kNN
from sklearn.svm import SVC  # support vector machine

c0 = ("Base Dummy", DummyClassifier(strategy='stratified'))
c1 = ("Decision Tree", DecisionTreeClassifier(max_depth=5))
c2 = ("Gaussian Naive Bayes", GaussianNB())
c3 = ("KNN", KNeighborsClassifier(n_neighbors=10))
#c4 = ("Support Vector Machines", SVC())

classifiers = [c0, c1, c2, c3]
In [ ]:
# @title  Predicción de la cantidad de dueños basándose en el precio y la puntuación en Metacritic
class_counts = df_games['estimated_owners'].value_counts()
valid_classes = class_counts[class_counts >= 2].index
data_filtered = df_games[df_games['estimated_owners'].isin(valid_classes)]

#separando atributos predictivos (X) del atributo objetivo (y)
X = data_filtered[['price','metacritic_score']].values
y = data_filtered['estimated_owners'].values

results = {}
for name, clf in classifiers:
    metrics = run_classifier(clf, X, y)   # hay que implementarla en el bloque anterior.
    results[name] = metrics
    print("----------------")
    print("Resultados para clasificador: ", name)
    print("Precision promedio:", np.array(metrics['precision']).mean())
    print("Recall promedio:", np.array(metrics['recall']).mean())
    print("F1-score promedio:", np.array(metrics['f1-score']).mean())
    print("----------------\n\n")
----------------
Resultados para clasificador:  Base Dummy
Precision promedio: 0.45293180402442973
Recall promedio: 0.45299701867252473
F1-score promedio: 0.4529592467956489
----------------


----------------
Resultados para clasificador:  Decision Tree
Precision promedio: 0.6047336565639093
Recall promedio: 0.7040640200847325
F1-score promedio: 0.6394325825118907
----------------


----------------
Resultados para clasificador:  Gaussian Naive Bayes
Precision promedio: 0.5723978257617705
Recall promedio: 0.5142397614938021
F1-score promedio: 0.5001481621798194
----------------


----------------
Resultados para clasificador:  KNN
Precision promedio: 0.6050389965558003
Recall promedio: 0.7016318845127886
F1-score promedio: 0.635961224535507
----------------


In [ ]:
# @title  Matriz de confusión
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
import numpy as np
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay

# Filtramos las clases válidas
class_counts = df_games['estimated_owners'].value_counts()
valid_classes = class_counts[class_counts >= 2].index
data_filtered = df_games[df_games['estimated_owners'].isin(valid_classes)]

# Separando atributos predictivos (X) del atributo objetivo (y)
X = data_filtered[['price', 'metacritic_score']].values
y = data_filtered['estimated_owners'].values

clf = DecisionTreeClassifier(max_depth=5)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30, random_state=15, stratify=y)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)

classes = unique_labels(y_test, y_pred)
conf_matrix = confusion_matrix(y_test, y_pred, labels=classes)
display = ConfusionMatrixDisplay(conf_matrix, display_labels=classes)

# Crear la figura y el eje para la matriz de confusión
fig, ax = plt.subplots(figsize=(9, 8))
display.plot(ax=ax)

# Rotar las etiquetas del eje x
plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")


# Mostrar el gráfico
plt.show()
In [ ]:
# @title  Predicción de la cantidad de dueños basándose en el precio y las reseñas positivas
class_counts = df_games['estimated_owners'].value_counts()
valid_classes = class_counts[class_counts >= 2].index
data_filtered = df_games[df_games['estimated_owners'].isin(valid_classes)]

X1 = data_filtered[['price','positive']].values
y1 = data_filtered['estimated_owners'].values

results1 = {}
for name, clf in classifiers:
    metrics = run_classifier(clf, X1, y1)   # hay que implementarla en el bloque anterior.
    results1[name] = metrics
    print("----------------")
    print("Resultados para clasificador: ", name)
    print("Precision promedio:", np.array(metrics['precision']).mean())
    print("Recall promedio:", np.array(metrics['recall']).mean())
    print("F1-score promedio:", np.array(metrics['f1-score']).mean())
    print("----------------\n\n")
----------------
Resultados para clasificador:  Base Dummy
Precision promedio: 0.4530556807642149
Recall promedio: 0.45343009571630305
F1-score promedio: 0.4532378235864337
----------------


----------------
Resultados para clasificador:  Decision Tree
Precision promedio: 0.7497584274632675
Recall promedio: 0.7819708143731368
F1-score promedio: 0.7573347346223646
----------------


----------------
Resultados para clasificador:  Gaussian Naive Bayes
Precision promedio: 0.2627310747470592
Recall promedio: 0.24435116899419415
F1-score promedio: 0.1579333562389433
----------------


----------------
Resultados para clasificador:  KNN
Precision promedio: 0.7366627779636193
Recall promedio: 0.7740467597677704
F1-score promedio: 0.7487414539848498
----------------


In [ ]:
# @title  Matriz de confusión
class_counts = df_games['estimated_owners'].value_counts()
valid_classes = class_counts[class_counts >= 2].index
data_filtered = df_games[df_games['estimated_owners'].isin(valid_classes)]

X1 = data_filtered[['price','positive']].values
y1 = data_filtered['estimated_owners'].values

clf = DecisionTreeClassifier(max_depth=5)
X_train, X_test, y_train, y_test = train_test_split(X1, y1, test_size=0.30, random_state=15, stratify=y1)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)

classes = unique_labels(y_test, y_pred)
conf_matrix = confusion_matrix(y_test, y_pred, labels=classes)
display = ConfusionMatrixDisplay(conf_matrix, display_labels=classes)

# Crear la figura y el eje para la matriz de confusión
fig, ax = plt.subplots(figsize=(9, 8))
display.plot(ax=ax)

# Rotar las etiquetas del eje x
plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")


# Mostrar el gráfico
plt.show()
In [ ]:
# @title  Predicción de la cantidad de dueños basándose en la fecha de lanzamiento y las recomendaciones

# Hacemos una copia del DataFrame original
df_games_copy = df_games.copy()

# Convertimos la columna release_date a datetime
df_games_copy['release_date'] = pd.to_datetime(df_games_copy['release_date'], errors='coerce')

# Extraemos características numéricas de la fecha
df_games_copy['year'] = df_games_copy['release_date'].dt.year
df_games_copy['month'] = df_games_copy['release_date'].dt.month
df_games_copy['day'] = df_games_copy['release_date'].dt.day

# Filtramos las clases válidas
class_counts = df_games_copy['estimated_owners'].value_counts()
valid_classes = class_counts[class_counts >= 2].index
data_filtered = df_games_copy[df_games_copy['estimated_owners'].isin(valid_classes)]

# Eliminamos las filas con NaN en las características seleccionadas
data_filtered = data_filtered.dropna(subset=['year', 'month', 'day', 'recommendations'])

# Preparamos las características y la variable objetivo
X2 = data_filtered[['year', 'month', 'day', 'recommendations']].values
y2 = data_filtered['estimated_owners'].values

results2 = {}
for name, clf in classifiers:
    metrics = run_classifier(clf, X2, y2)   # hay que implementarla en el bloque anterior.
    results2[name] = metrics
    print("----------------")
    print("Resultados para clasificador: ", name)
    print("Precision promedio:", np.array(metrics['precision']).mean())
    print("Recall promedio:", np.array(metrics['recall']).mean())
    print("F1-score promedio:", np.array(metrics['f1-score']).mean())
    print("----------------\n\n")
----------------
Resultados para clasificador:  Base Dummy
Precision promedio: 0.45303906592942894
Recall promedio: 0.453051545582928
F1-score promedio: 0.4530403169242495
----------------


----------------
Resultados para clasificador:  Decision Tree
Precision promedio: 0.616086940999507
Recall promedio: 0.6820178879648516
F1-score promedio: 0.6055770806871238
----------------


----------------
Resultados para clasificador:  Gaussian Naive Bayes
Precision promedio: 0.12177468346275867
Recall promedio: 0.2011611485956379
F1-score promedio: 0.09135724986895451
----------------


----------------
Resultados para clasificador:  KNN
Precision promedio: 0.6166905569975587
Recall promedio: 0.6709948219049114
F1-score promedio: 0.62973466092412
----------------


In [ ]:
# @title  Matriz de confusión
# Hacemos una copia del DataFrame original
df_games_copy = df_games.copy()

# Convertimos la columna release_date a datetime
df_games_copy['release_date'] = pd.to_datetime(df_games_copy['release_date'], errors='coerce')

# Extraemos características numéricas de la fecha
df_games_copy['year'] = df_games_copy['release_date'].dt.year
df_games_copy['month'] = df_games_copy['release_date'].dt.month
df_games_copy['day'] = df_games_copy['release_date'].dt.day

# Filtramos las clases válidas
class_counts = df_games_copy['estimated_owners'].value_counts()
valid_classes = class_counts[class_counts >= 2].index
data_filtered = df_games_copy[df_games_copy['estimated_owners'].isin(valid_classes)]

# Eliminamos las filas con NaN en las características seleccionadas
data_filtered = data_filtered.dropna(subset=['year', 'month', 'day', 'recommendations'])

# Preparamos las características y la variable objetivo
X2 = data_filtered[['year', 'month', 'day', 'recommendations']].values
y2 = data_filtered['estimated_owners'].values

clf = DecisionTreeClassifier(max_depth=5)
X_train, X_test, y_train, y_test = train_test_split(X2, y2, test_size=0.30, random_state=15, stratify=y2)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)

classes = unique_labels(y_test, y_pred)
conf_matrix = confusion_matrix(y_test, y_pred, labels=classes)
display = ConfusionMatrixDisplay(conf_matrix, display_labels=classes)

# Crear la figura y el eje para la matriz de confusión
fig, ax = plt.subplots(figsize=(9, 8))
display.plot(ax=ax)

# Rotar las etiquetas del eje x
plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")


# Mostrar el gráfico
plt.show()
In [ ]:
# @title  Predicción de la cantidad de dueños basándose en reseñas positivas y negativas
class_counts = df_games['estimated_owners'].value_counts()
valid_classes = class_counts[class_counts >= 2].index
data_filtered = df_games[df_games['estimated_owners'].isin(valid_classes)]

X3 = data_filtered[['positive','negative']].values
y3 = data_filtered['estimated_owners'].values

results3 = {}
for name, clf in classifiers:
    metrics = run_classifier(clf, X3, y3)   # hay que implementarla en el bloque anterior.
    results3[name] = metrics
    print("----------------")
    print("Resultados para clasificador: ", name)
    print("Precision promedio:", np.array(metrics['precision']).mean())
    print("Recall promedio:", np.array(metrics['recall']).mean())
    print("F1-score promedio:", np.array(metrics['f1-score']).mean())
    print("----------------\n\n")
----------------
Resultados para clasificador:  Base Dummy
Precision promedio: 0.45296304648569724
Recall promedio: 0.4532806370626078
F1-score promedio: 0.4531167358785427
----------------


----------------
Resultados para clasificador:  Decision Tree
Precision promedio: 0.7029434610039946
Recall promedio: 0.7033971442021025
F1-score promedio: 0.68788219727119
----------------


----------------
Resultados para clasificador:  Gaussian Naive Bayes
Precision promedio: 0.10356141353952113
Recall promedio: 0.2256786442805586
F1-score promedio: 0.1153237190476415
----------------


----------------
Resultados para clasificador:  KNN
Precision promedio: 0.6975135052229418
Recall promedio: 0.6999450808096661
F1-score promedio: 0.6850103852704434
----------------


In [ ]:
# @title  Matriz de confusión
class_counts = df_games['estimated_owners'].value_counts()
valid_classes = class_counts[class_counts >= 2].index
data_filtered = df_games[df_games['estimated_owners'].isin(valid_classes)]

X3 = data_filtered[['positive','negative']].values
y3 = data_filtered['estimated_owners'].values

clf = DecisionTreeClassifier(max_depth=5)
X_train, X_test, y_train, y_test = train_test_split(X3, y3, test_size=0.30, random_state=15, stratify=y3)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)

classes = unique_labels(y_test, y_pred)
conf_matrix = confusion_matrix(y_test, y_pred, labels=classes)
display = ConfusionMatrixDisplay(conf_matrix, display_labels=classes)

# Crear la figura y el eje para la matriz de confusión
fig, ax = plt.subplots(figsize=(9, 8))
display.plot(ax=ax)

# Rotar las etiquetas del eje x
plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")


# Mostrar el gráfico
plt.show()

Conclusión¶

  Nuevamente, con respecto a los clasificadores seleccionados, ignoraremos Base Dummy al ser un método básico para clasificar, presente más que nada para referencia (asegurarnos de tener un mínimo aproximado para los valores de Precision, Recall y F1-Score).

En el caso de Gaussian Naive Bayes, se puede observar un desempeño ineficiente. Este resultado se pudo haber dado por una posible dependencia entre los atributos, lo cual tendría conflictos con la metodología de la técnica usada.

Es por esto que interpretaremos y analizaremos la información obtenida programáticamente utilizando los clasificadores Decision Tree y KNN.

Con respecto a los datos, se observan que los atributos se encuentran en clases desbalanceadas, por lo que, para todo intento de estudio con estos clasificadores, las métricas como Accuracy, Precision, Recall y F1-Score están dando resultados dominados por la clase [0, 20000]. No se tuvo el tiempo suficiente para seguir experimentando habiendo hecho un subsampling (donde suponemos que se obtendrían resultados más equilibrados) y comparar resultados.

Además, a la hora de obtener resultados, hemos únicamente clasificado para cuatro pares de atributos, habiendo otros como "Average Playtime" (tiempo promedio de juego por los usuarios) o "User Score" (porcentaje de las reviews positivas con respecto al total). No consideramos las combinaciones escogidas como totalmente concluyentes, pero nos entrega una buena noción de qué parámetros influyen más a la hora de entregar información a un desarrollador de juegos.

En vista de los resultados programáticos que hemos obtenido, a un desarrollador de juegos le es más relevante la información obtenida a partir de las clases “precio del juego”, “cantidad de reseñas positivas” y "cantidad de reseñas negativas", información útil para hacer que su juego tenga más impacto y que más usuarios se conviertan en posibles compradores. Con esto, el desarrollador tendrá una idea más clara de en qué parámetros enfocarse si desea llegar a un rango de ventas específico, y con eso comparar si su producto fue un éxito o un fracaso en la fecha de lanzamiento.

Con este método es complicado comparar los "Pesos" que tiene cada atributo al momento de predecir las ventas, ya que la cantidad de clasificaciones manuales sería muy alta. La mejor forma de abordar este problema sería realizar algún metodo de selección de atributos (PCA) o de reducción de atributos.

Pregunta 2¶

A continuación se muestra el la experimentación hecha para responder la pregunta 2.

In [ ]:
# @title  Preprocesamiento

def preprocessing(dataframe, genre, begin_year = 1997, target_year = 2025):
  # Asegurarse de que la columna 'release_date' esté en formato de fecha
  dataframe['release_date'] = pd.to_datetime(dataframe['release_date'], errors='coerce')

  # Extraer el año de la fecha de lanzamiento
  dataframe['year'] = dataframe['release_date'].dt.year
  dataframe = dataframe[(dataframe['year'] >= begin_year) & (target_year >= dataframe['year'])]

  # Filtrar los datos para eliminar aquellos sin año de lanzamiento válido
  dataframe = dataframe.dropna(subset=['year'])
  # Extraemos los géneros
  dataframe = dataframe[dataframe['genres'].str.contains(genre, na=False)]
  result = dataframe.sort_values(by=["year"])[["year","genres"]]

  instances = result.groupby("year")

  # grupos [[años],[cantidades]]
  groups = [[],[]]
  for a, b in instances:
    groups[0].append(a)
    groups[1].append(b["genres"].size)
  #print(len(groups[0]))
  #print(len(groups[1]))
  final_result = pd.DataFrame({"year": groups[0], "quantity": groups[1]})
  return final_result

 Para el preprocesamiento convertiremos las fechas en la columna "Release date" en un formato numérico con el cual podamos trabajar. Luego, para un género elegido seleccionaremos los juegos entre ciertos años. Para ejemplicar, tomaremos los juegos de del género de acción (Action) lanzados entre 1997 y 2025 (este último año puede deberse a juegos que están en acceso anticipado y serán lanzados oficialmente en dicha fecha).

In [ ]:
# @title  Gráfico cantidad de juegos por género "Action" a través de los años
test1 = preprocessing(df_games, "Action")


X = []
y = []

X.extend(test1['year'].values)
y.extend(test1['quantity'].values)

X = np.array(X).reshape(-1, 1)
y = np.array(y).reshape(-1, 1)
plt.xlabel('Años')
plt.ylabel('Cantidad de juegos"')
plt.title('Cantidad de juegos por género "Action" a través de los años')
plt.scatter(X, y)
plt.show()

Se puede observar un crecimiento sostenido, casi lineal, desde el año 2013 hasta el 2024, con una pequeña baja para el 2019. Los años previos a 2013 podrían obstaculizar el análisis al tener valores muy bajos con respecto a los recientes. Pero, ¿qué pasa con 2024 o 2025? Ambos años podrían ser obstáculos para el análisis. El primer año porque aún no ha terminado, por lo que no representará correctamente la cantidad de videojuegos lanzados. Mientras que el segundo aún no ha empezado.

In [ ]:
# @title  Gráfico cantidad de juegos por género "Action" a través de los años restringidos
test2 = preprocessing(df_games, "Action", 2013, 2023)


X = []
y = []

X.extend(test2['year'].values)
y.extend(test2['quantity'].values)

X = np.array(X).reshape(-1, 1)
y = np.array(y).reshape(-1, 1)
plt.xlabel('Años')
plt.ylabel('Cantidad de juegos"')
plt.title('Cantidad de juegos por género "Action" entre 2013 y 2023')
plt.scatter(X, y)
plt.show()

Si acotamos el rango de los años, se evidencia de manera más clara un comportamiento lineal, lo que respalda la decisión de utilizar una regresión lineal.

Un primer acercamiento¶

In [ ]:
# @title  Experimento (regresión lineal)
# Supongamos que tus archivos se llaman 'data1.csv', 'data2.csv', ..., 'data10.csv'
# resultadosHeap/Fib

def fitting(dataframe, x_column, y_column, begin_year, target_year):
  # Creamos una lista para almacenar los resultados de cada regresión lineal
  X = []
  y = []

  # Definimos nuestras variables independientes y dependientes
  X.extend(dataframe[x_column].values) #independiente año
  y.extend(dataframe[y_column].values) #dependiente cantidad de juegos por el genero pedido

  X = np.array(X).reshape(-1, 1)
  y = np.array(y).reshape(-1, 1)

  # Creamos una instancia de la clase LinearRegression
  regressor = LinearRegression()

  # Entrenamos nuestro modelo con los datos de entrenamiento
  regressor.fit(X, y)

  # Almacenamos los resultados
  resultados = (regressor.coef_[0][0], regressor.intercept_[0])

  pendiente = resultados[0]
  intercepto = resultados[1]

  # Creamos un array de valores x basado en los datos originales
  x_plot = np.linspace(begin_year, target_year)
  # Calculamos los valores y usando la ecuación de la línea
  y_plot = pendiente * x_plot + intercepto

  print("Valor estimado para " + str(target_year) + ": " + str(pendiente * target_year + intercepto))
  print("Error absoluto medio:", mean_absolute_error(y, regressor.predict(X)))
  print("Coeficiente de determinación (R^2):", r2_score(y, regressor.predict(X)))

  plt.xlabel('Años')
  plt.ylabel('Cantidad de juegos')
  plt.title('Regresión lineal para la cantidad de juegos' + '\n' +  'por género "Action" entre ' + str(begin_year) + ' y ' + str(target_year))

  plt.scatter(X, y)
  plt.plot(x_plot, y_plot, color='red')
  plt.show()

  return resultados
In [ ]:
# @title  Gráfico regresión lineal cantidad de juegos por género "Action" a través de los años
fitting1 = fitting(test1, "year", "quantity", 1997, 2025)
print('')
fitting2= fitting(test2, "year", "quantity", 2013, 2025)
Valor estimado para 2025: 3171.744827586226
Error absoluto medio: 1014.5130966536456
Coeficiente de determinación (R^2): 0.45026782143598865
Valor estimado para 2025: 6820.518181818072
Error absoluto medio: 144.1818181817547
Coeficiente de determinación (R^2): 0.986741767958694
  • Para el primer gráfico, si tomamos en cuenta todos los datos, visualmente la regresión lineal no predice de buena manera la cantidad de videojuegos para el 2025, ya que la pendiente calculada es mucho más baja de lo esperado y no alcanza a los valores reales. Esto puede verse reflejado en el coeficiente de determinación, el cual está muy alejado de uno.

  • Si no consideramos los años anteriores al 2013 y posteriores al 2023, se observa una regresión con mejor desempeño y se nota de una mejor manera el comportamiento lineal de los datos. El error absoluto medio es mucho menor al del gráfico anterior y el coeficiente de determinación muestra una predicción mucho más acertada, por lo que este modelo es el que mejor predice el valor buscado.

¿Existirá otro tipo de regresión para analizar estos datos?¶

  • Tras ver el gráfico de los datos sin retringir los años vemos que, a simple vista, se asemeja a un crecimiento exponencial. Es por esto que usaremos regresión exponencial para ver si nos da una predicción aún más acertada.

In [ ]:
# @title  Experimiento (regresión exponencial)

def fittingExpNormalized(dataframe, x_column, y_column, begin_year, target_year):
  # Creamos una lista para almacenar los resultados de cada regresión lineal
  X = dataframe[x_column].values
  y = dataframe[y_column].values

  # Normalizamos los años para evitar problemas de desbordamiento
  X_normalized = (X - np.mean(X)) / np.std(X)

  # Tomamos el logaritmo de y
  y_log = np.log(y)

  # Creamos una instancia de la clase LinearRegression
  regressor = LinearRegression()

  # Entrenamos nuestro modelo con los datos de entrenamiento
  regressor.fit(X_normalized.reshape(-1, 1), y_log.reshape(-1, 1))

  # Almacenamos los resultados
  pendiente = regressor.coef_[0][0]
  intercepto = regressor.intercept_[0]

  # Creamos un array de valores x basado en los datos originales
  x_plot = np.linspace(begin_year, target_year, 100)
  x_plot_normalized = (x_plot - np.mean(X)) / np.std(X)

  # Calculamos los valores y usando la ecuación de la línea
  y_plot = np.exp(intercepto) * np.exp(pendiente * x_plot_normalized)

  y_log_pred = regressor.predict(X_normalized.reshape(-1, 1))
  y_pred = np.exp(y_log_pred)
  mae_original = mean_absolute_error(y, y_pred)

  print("Valor estimado para " + str(target_year) + ": " + str(np.exp(intercepto) * np.exp(pendiente * (target_year - np.mean(X)) / np.std(X))))
  print("Error absoluto medio:",mae_original)
  print("Coeficiente de determinación (R^2):", r2_score(y, y_pred))

  plt.xlabel('Años')
  plt.ylabel('Cantidad de juegos')
  plt.title('Regresión exponencial para la cantidad de juegos' + '\n' + 'por género "Action" entre ' + str(begin_year) + ' y ' + str(target_year))
  plt.scatter(X, y)
  plt.plot(x_plot, y_plot, color='red')
  plt.show()

  return (pendiente, intercepto)
In [ ]:
# @title  Gráfico regresión lineal cantidad de juegos por género "Action" a través de los años restringidos
fitting1 = fittingExpNormalized(test1, "year", "quantity", 1997, 2025)
print('')
test3 = preprocessing(df_games, "Action", 1997, 2023)
fitting3 = fittingExpNormalized(test3, "year", "quantity", 1997, 2025)
print('')
fitting2 = fittingExpNormalized(test2, "year", "quantity", 2013, 2025)
Valor estimado para 2025: 3961.5252809066546
Error absoluto medio: 1077.242431970045
Coeficiente de determinación (R^2): 0.014076494783377913
Valor estimado para 2025: 29187.729019722865
Error absoluto medio: 706.374517885078
Coeficiente de determinación (R^2): 0.02550310201499939
Valor estimado para 2025: 14614.965472725622
Error absoluto medio: 789.8463072633757
Coeficiente de determinación (R^2): 0.5943960592235875
  • Como era de esperar, los resultados no se acercan lo suficiente a los datos reales como para considerar este tipo de regresión, devolviendo valores de error absoluto medio muy altos y coeficientes $R^2$ muy bajos con respecto a el modelo lineal.

Conclusión¶

 Volviendo a la pregunta inicial: ¿Podemos predecir la cantidad de juegos de un género que saldrán un año determinado? La respuesta es sí, la cantidad de videojuegos lanzados por año se puede predecir y con una buena presición. Sin embargo, para obtener una buena predicción será necesario acotar el rango de los años para obtener una muestra más representativa puesto que antiguamente no se usaba tanto Steam como hoy en día.

Pregunta 3¶

Lo primero que haremos será normalizar los datos antes de trabajarlos. Esto es, el normalizador recibirá los datos de la tabla, calculará la media y desviación estándar, y normalizará las tuplas de modo que los valores queden con Media 0, Desviación Estándar 1. Esto nos permitirá trabajar los datos para encontrar los clusters.

In [ ]:
# @title  Preprocesamiento

import pandas as pd
import numpy as np

# Eliminar filas con valores nulos en columnas críticas
df_games.dropna(subset=['price', 'genres', 'release_date'], inplace=True)

# Filtrar filas donde 'positive', 'negative' y recommendations sean mayores que 0
df_games = df_games[(df_games['positive'] > 0) & (df_games['negative'] > 0) & (df_games['recommendations'] > 0)]

# Filtrar datos relevantes
df_games = df_games[['app_id', 'name', 'genres', 'price', 'positive', 'negative', 'recommendations']]

# Obtener género principal
df_games['main_genre'] = df_games['genres'].apply(lambda x: x.split(',')[0].strip())

df_games
Out[ ]:
app_id name genres price positive negative recommendations main_genre
10 1026420 WARSAW Indie,RPG 23.99 589 212 427 Indie
15 22670 Alien Breed 3: Descent Action 9.99 349 134 285 Action
17 346560 Hero of the Kingdom II Adventure,Casual,Indie,RPG 7.99 2046 120 1615 Adventure
22 434030 Aerofly FS 2 Flight Simulator Action,Indie,Racing,Simulation 37.49 1490 408 1831 Action
24 2073470 Kanjozoku Game レーサー Massively Multiplayer,Racing,Simulation,Sports 5.99 392 57 493 Massively Multiplayer
... ... ... ... ... ... ... ... ...
84311 2152790 UNITED 1944 Action,Early Access 23.99 160 30 179 Action
84325 2052410 WITCH ON THE HOLY NIGHT Adventure 35.99 658 13 740 Adventure
84326 2287520 Five Nights at Freddy's: Help Wanted 2 Indie,Simulation 39.99 862 84 1067 Indie
84454 2499800 Garten of Banban 6 Action,Adventure,Casual,Indie 9.99 280 123 403 Action
84963 2487350 Alex Jones: NWO Wars Action,Indie 17.76 596 32 780 Action

13208 rows × 8 columns

Una vez tengamos los datos normalizados, podemos aplicar PCA a los datos para encontrar la mejor combinación de atributos a trabajar. Así, podemos encontrar los atributos a entender que sirven más para un desarrollador a la hora de crear su juego.

In [ ]:
# @title  PCA
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

X = df_games[['price', 'positive', 'negative', 'recommendations']]

# Escriba su código aquí
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)

# Visualizar los datos
plt.scatter(X_pca[:, 0], X_pca[:, 1])
plt.show()

Sumado a esto, aplicaremos el método del codo para encontrar cuál es la cantidad de clusters ideal para estos datos. Visualmente podemos reconocer un gran cluster, seguido de 3 de menor tamaño, por lo que es esperado que el análisis del gráfico nos entregue resultados similares (sería de esperar que el codo se encuentre entre 3 y 4 clusters).

In [ ]:
# @title  Eligiendo el número de clusters óptimo
from sklearn.cluster import KMeans

# Escriba su código aquí
sse = []

clusters = list(range(1, 16))
for k in clusters:
    kmeans = KMeans(n_clusters=k, n_init='auto').fit(X_pca)
    sse.append(kmeans.inertia_)

plt.plot(clusters, sse, marker="o")
plt.title("Metodo del codo, desde 1 hasta 15 clusters")
plt.grid(True)
plt.show()

Según habíamos dicho antes de manipular los datos con el método del codo, el gráfico se alinea con lo que habíamos concluído, y lo ideal sería considerar 4 clusters para este problema. Si bien sería posible trabajar con 3 clusters en vez de 4, el SSE baja lo suficiente para considerar trabajar con 4.

Con esto dicho, decidimos escoger 4 clusters y, habiendo decidido esto, podemos aplicar un clasificador como K-means para visualizar los clusters escogidos.

In [ ]:
# @title  K-means
random_state = 20

kmeans_4 = KMeans(n_clusters=4, n_init='auto', random_state=random_state).fit(X_pca)

centers_4 = kmeans_4.cluster_centers_

labels = kmeans_4.labels_

plt.scatter(X_pca[:, 0], X_pca[:, 1], c=kmeans_4.labels_)
plt.scatter(centers_4[:,0], centers_4[:,1], s=200, facecolors='none', edgecolors='r')
plt.title("K-Means")
plt.show()

Considerando que el gráfico está "estirado" (es decir, una distancia d en el eje x es mucho menos significativa que en el eje y), los 4 Clusters son fáciles de divisar, aunque uno de los clusters posee mucho ruido, y el otro es prácticamente un outlier.

En base a lo recién realizado, se seleccionan juegos de cada cluster para buscar identificar patrones en los juegos de cada cluster.

In [ ]:
# @title  
# Añade las etiquetas de los clusters al DataFrame original
df_games['cluster'] = labels

# Asigna nombres a los clusters basados en el análisis
cluster_names = {
    0: "Test0",
    1: "Test1",
    2: "Test2",
    3: "Test3"
}

# Mapea los nombres de los clusters a las etiquetas
df_games['cluster_name'] = df_games['cluster'].map(cluster_names)

# Mostrar ejemplos de cada cluster
for name, group in df_games.groupby('cluster_name'):
    print(f"Cluster: {name}")
    print(group[['name', 'main_genre', 'price', 'positive', 'negative', 'recommendations']].head())
    print("\n")
Cluster: Test0
                             name             main_genre  price  positive  \
10                         WARSAW                  Indie  23.99       589   
15         Alien Breed 3: Descent                 Action   9.99       349   
17         Hero of the Kingdom II              Adventure   7.99      2046   
22  Aerofly FS 2 Flight Simulator                 Action  37.49      1490   
24            Kanjozoku Game レーサー  Massively Multiplayer   5.99       392   

    negative  recommendations  
10       212              427  
15       134              285  
17       120             1615  
22       408             1831  
24        57              493  


Cluster: Test1
                                   name main_genre  price  positive  negative  \
46158  Counter-Strike: Global Offensive     Action    0.0   5764420    766677   

       recommendations  
46158          3441592  


Cluster: Test2
                                 name main_genre  price  positive  negative  \
1289                      Garry's Mod      Indie   9.99    822326     29004   
2904  Tom Clancy's Rainbow Six® Siege     Action  19.99    312232     64137   
4287  Tom Clancy's Rainbow Six® Siege     Action  19.99    312816     64201   
5993            ARK: Survival Evolved     Action  29.99    461567     98701   
8009                   Cyberpunk 2077        RPG  59.99    391643    129925   

      recommendations  
1289           725462  
2904           899435  
4287           899455  
5993           435328  
8009           458744  


Cluster: Test3
                         name main_genre  price  positive  negative  \
47                 Far Cry® 5     Action  59.99    100620     25286   
57            Forza Horizon 4     Racing  59.99    122539     15095   
96        Oxygen Not Included      Indie  24.99     82902      3014   
736             Apex Legends™     Action   0.00    415524     66608   
828  American Truck Simulator      Indie  19.99    104521      3859   

     recommendations  
47            114588  
57            126316  
96             80467  
736             1000  
828            87888  


En general los Clusters "Test[$i$]", para $i$ = {0, 2, 3}, no dan mucha información sobre los juegos que contienen, pero suele estar muy relacionado con la magnitud de recomendaciones y reviews en general que poseen. Por otro lado, notamos que el juego "Counter Strike: Global Offensive" es nuestro outlier a la derecha del gráfico, con una cantidad absurda de recomendaciones y de reviews positivas en comparación con los otros clusters.

A continuación, por la alta cantidad de datos se decidió extraer un sample al azar del 10% del total de los datos. Con esto, podemos aplicar Aglomerative Clustering y DBSCAN. Dicho esto, se comenzó realizando el sampling y trabajando dichos datos para Linkage: Ward.

In [ ]:
# @title  Sample

# Muestreo aleatorio del 10% de los datos
sample_fraction = 0.1
df_sample = df_games.sample(frac=sample_fraction, random_state=42)
X_sample = df_sample[['price', 'positive', 'negative', 'recommendations']]
X_sample_pca = pca.transform(X_sample)
In [ ]:
# @title  
from scipy.cluster.hierarchy import dendrogram, linkage

complete = linkage(X_sample, method="complete")
single = linkage(X_sample, method="single")
average = linkage(X_sample, method="average")
ward = linkage(X_sample, method="ward")
In [ ]:
# @title  
dendrogram(ward)
plt.title("Linkage: Ward")
plt.show()
In [ ]:
# @title  
dendrogram(ward)
plt.title("Linkage: Ward")
plt.axhline(y=400000, color='r', linestyle='--')
plt.show()

Notemos que, en la parte superior del sample, podemos ver que la franja azul se divide en dos clusters para una altura superior a 800 mil. Se realiza esta aclaración puesto que es difícil ver a simple vista el cluster izquierdo, el cual está pegado al eje y, dificultando el poder verlo.

Luego, viendo la distancia de las ramas a la raíz del dendograma, se consideró pertinente elegir una altura de corte en x = 400 mil. Con esto es directo que la cantidad de clusters resultantes es 3.

In [ ]:
# @title  
from sklearn.cluster import AgglomerativeClustering

# Escriba su código aquí
ward_all = AgglomerativeClustering(n_clusters=None, linkage="ward", distance_threshold=400000).fit(X_sample_pca)
print(ward_all.n_clusters_)
3

Se puede ver que una rápida verificación programática nos entrega el mismo resultado con respecto al número de clusters.

Luego, podemos usar el mismo linkage para Clustering Jerárquico:

In [ ]:
# @title  
plt.scatter(X_sample_pca[:, 0], X_sample_pca[:, 1], c=ward_all.labels_)
plt.title("Hierarchical: ward, 3 clusters")
plt.show()

Esta vez, los clusters claramente tienen una estructura que se asemeja más a un clustering en función de densidades que sobre distancias. Sin embargo, una consecuencia directa de esto es el tercer cluster (color morado), cuyos valores están muy dispersos a lo largo del gráfico (tiene altos niveles de ruido).

Finalmente, revisaremos también el modelo de clasificador DBSCAN, el cual trabaja en base a densidad de nodos:

In [ ]:
# @title  
from sklearn.neighbors import NearestNeighbors
import numpy as np

nbrs = NearestNeighbors(n_neighbors=3).fit(X_sample)
distances, indices = nbrs.kneighbors(X_sample)

distances = np.sort(distances, axis=0)
distances = distances[:,1]

fig, ax = plt.subplots()

ax.axhline(y=5000, color='r', linestyle='--') # Ajuste el valor para y
ax.plot(distances)

plt.show()

Visualmente podemos determinar que el valor de eps ronda los 5000. Se guiará la construcción del clasificador DBSCAN bajo lo visto en Laboratorios, tal que usamos minPts igual a 5. Así:

In [ ]:
# @title  
from sklearn.cluster import DBSCAN

eps = 5000
min_samples = 5

dbscan = DBSCAN(eps=eps, min_samples=min_samples).fit(X_sample_pca)
plt.scatter(X_sample_pca[:,0], X_sample_pca[:,1], c=dbscan.labels_)
plt.title(f"DBSCAN: eps={eps}, min_samples={min_samples}")
plt.show()

Notamos que DBSCAN encuentra 4 clusters dentro de los datos proporcionados, donde es difícil concluir mucho sobre el cluster de color morado, pues sus valores están tan dispersos que podrían confundirse con ruido. Sin embargo, los otros 3 clusters se ven bien definidos.

Finalmente, se evaluarán los clasificadores utilizados mediante el Coeficiente de Silhouette (se recuerda que, entre más cercano este coeficiente a 1.0, mejor es el modelo):

In [ ]:
# @title  Coeficiente de Silhouette
from sklearn.metrics import silhouette_score

print("Dataset X K-Means 4\t", silhouette_score(X_pca, kmeans_4.labels_))

print("Dataset X ward all\t", silhouette_score(X_sample_pca, ward_all.labels_))

_filter_label = dbscan.labels_ >= 0
print("Dataset X DBSCAN\t", silhouette_score(X_sample_pca[_filter_label], dbscan.labels_[_filter_label]))
Dataset X K-Means 4	 0.9552412648896319
Dataset X ward all	 0.9199589324743636
Dataset X DBSCAN	 0.906227566420273

De esta manera, es directo que K-Means para n = 4 clusters resulta ser el clasificador más eficiente para los datos propiciados.

Conclusión¶

 En base a todo lo desarrollado con respecto a esta pregunta, se considera acertado decir que los juegos del dataset, bajo los atributos escogidos, tienden a separarse en grupos distinguibles entre sí. Si bien la proporción de recomendaciones a cantidad de reviews positivas suele ser universal y muy similar entre grupos, lo importante a considerar es la cantidad de recomendaciones y en general reviews por juego individual, donde es fácil distinguir que cada grupo tiene estas cantidades en intervalos bien definidos.

Planificación futura¶

Como grupo se determinó que, en un escenario hipotético donde el problema se vuelve a visitar en el futuro a causa de tener más tiempo, el problema se podría seguir trabajando bajo las siguientes aristas:

  • Usar clasificadores para predecir precios acorde a las características de un juego

  • Encontrar alguna otra metodología para responder a la primera pregunta

  • Abordar el tema desde la perspectiva de un consumidor

Contribuciones¶

Contribuciones de cada integrante Hito 1¶

  • Adolfo Arenas: Creación del Canva(PPT), el diseño y encargado de cargar los datos desde Google Drive. Trabajó Wordcloud de géneros e histograma.
  • Alejandro Mori: A cargo de la redacción de las preguntas y los motivos. Trabajó Wordcloud de géneros e histograma.
  • Ignacio Humire: Encargado de la limpieza de los datos, además del diseño del PPT. Analiza y comenta el scatterplot.
  • Leonardo Rikhardsson: Estuvo a cargo de la creación del WordCloud de géneros por año. Analiza y comenta el scatterplot.
  • Mario Benavente: Redactar cada parte de los análisis y Exploración de datos. Analiza y comenta el scatterplot.

Contribuciones de cada integrante Hito 2¶

  • Adolfo Arenas: Encargado del código de la pregunta 1, además de explicar la metodología de la pregunta 3.
  • Alejandro Mori: Encargado de parte del código de la pregunta 1, además de explicar la metodología de la pregunta 2.
  • Ignacio Humire: Encargado de parte del código de la pregunta 1, además analiza y comenta los resultados obtenidos.
  • Leonardo Rikhardsson: Encargado de parte del código de la pregunta 1, además de explicar la metodología de la pregunta 3.
  • Mario Benavente: Encargado de parte del código de la pregunta 1,además analiza y comenta los resultados obtenidos.

Contribuciones de cada integrante Hito 3¶

  • Adolfo Arenas: Creación de código para del experimento de la pregunta 2.
  • Alejandro Mori: Redactar cada parte de los análisis y Exploración de datos para la pregunta 2.
  • Ignacio Humire: Encargado de la limpieza de los datos, además del preprocesamiento de datos.
  • Leonardo Rikhardsson: Creación del Canva (PPT) y el código usado en el experimento de la pregunta 3.
  • Mario Benavente: Redactar cada parte de los análisis y Exploración de datos para la pregunta 3.

    Adicionalmente, todos los integrantes ayudaron a la confección del PPT a presentar ante los profesores.

Anexos¶

Steam reviews. (2023, noviembre 9). Kaggle.com; Kaggle. https://www.kaggle.com/code/gonzafrancoandres/steam-reviews

How to do exponential and logarithmic curve fitting in Python?. Stack Overflow. https://stackoverflow.com/questions/3433486/how-to-do-exponential-and-logarithmic-curve-fitting-in-python-i-found-only-poly